﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PuyoNeuron
{
    class CodeModel
    {
        #region private classes
        public class LuaClass
        {
            public string Constructor { get; set; }
            public string Code { get; set; }

            public override string ToString()
            {
                return Code.Trim() + "\n";
            }
        }

        public class LuaInstance
        {
            // TODO: instances should be allowed local constants later... i think?
            public string InstanceID { get; set; }
            public LuaClass Class { get; set; }

            public override string ToString()
            {
                return InstanceID + " = " + Class.Constructor;
            }
        }

        public class EmitConnection
        {
            public string ReceiveDereference { get; set; }
            public string EmitDereference { get; set; }

            public override string ToString()
            {
                return "function " + ReceiveDereference + "(...) return " + EmitDereference + "(...) end";
            }
        }

        public class FireConnection
        {
            public string FireDereference { get; set; }
            public List<string> TriggerDereference { get; set; }

            public FireConnection()
            {
                TriggerDereference = new List<string>();
            }

            public override string ToString()
            {
                StringBuilder lCode = new StringBuilder();
                lCode.AppendLine("function " + FireDereference + "(...)");

                foreach (string lTrigger in TriggerDereference)
                    lCode.AppendLine("  " + lTrigger + "(...)");

                lCode.AppendLine("end");
                return lCode.ToString().Trim() + "\n"; ;
            }
        }
        #endregion

        Dictionary<LuaClass, int> mClasses;
        List<LuaInstance> mInstances;
        List<EmitConnection> mEmits;
        List<FireConnection> mFires;
        List<string> mStart;
        static CodeModel sInstance;

        bool mDebug = true;

        public List<string> Start
        {
            get { return mStart; }
            set { mStart = value; }
        }

        public static CodeModel Instance
        {
            get { return sInstance = sInstance ?? new CodeModel(); }
        }

        private CodeModel()
        {
            mClasses = new Dictionary<LuaClass, int>();
            mInstances = new List<LuaInstance>();
            mEmits = new List<EmitConnection>();
            mFires = new List<FireConnection>();
            mStart = new List<string>();
        }

        private void debug(string pLine)
        {
            if (mDebug)
                System.Diagnostics.Debug.WriteLine(pLine);
        }

        #region synchronize with panel objects... i guess?
        public void Connect(Panel.Nub pNubA, Panel.Nub pNubB)
        {
            // if they're backwards switch them cuz its easier than thinking about it later
            if (pNubA.Type != Panel.Nub.NubType.Fire && pNubA.Type != Panel.Nub.NubType.Emitter)
            {
                Panel.Nub lTemp = pNubA;
                pNubA = pNubB;
                pNubB = lTemp;
            }

            if (pNubA.Type == Panel.Nub.NubType.Emitter)
            {
                EmitConnection lEmit = new EmitConnection();
                lEmit.EmitDereference = pNubA.Parent.Title + ":" + pNubA.Name;
                lEmit.ReceiveDereference = pNubB.Parent.Title + ":" + pNubB.Name;
                mEmits.Add(lEmit);

                debug("connecting emitter " + lEmit.EmitDereference + " to receiver " + lEmit.ReceiveDereference);
            }
            else
            {
                string lFireDereference = pNubA.Parent.Title + ":" + pNubA.Name;

                FireConnection lFire = null;
                foreach (FireConnection lSearchFire in mFires.Where(delegate(FireConnection pSearchFire) { return pSearchFire.FireDereference == lFireDereference; }))
                    lFire = lSearchFire;

                if (lFire == null)
                {
                    debug("adding new fire connection pool for " + lFireDereference);
                    lFire = new FireConnection();
                    lFire.FireDereference = lFireDereference;
                    mFires.Add(lFire);
                }

                lFire.TriggerDereference.Add(pNubB.Parent.Title + ":" + pNubB.Name);

                debug("connecting fire " + lFire.FireDereference + " to trigger " + lFire.TriggerDereference);
            }

            int lTotalTriggers = 0;
            foreach (FireConnection lFireConnection in mFires)
                lTotalTriggers += lFireConnection.TriggerDereference.Count();

            debug("total emits: " + mEmits.Count());
            debug("total fire pools: " + mFires.Count());
            debug("total fires: " + lTotalTriggers);
            debug("---------------------------");
        }

        public void Break(Panel.Nub pNubA, Panel.Nub pNubB)
        {
            // if they're backwards switch them cuz its easier than thinking about it later
            if (pNubA.Type != Panel.Nub.NubType.Fire && pNubA.Type != Panel.Nub.NubType.Emitter)
            {
                Panel.Nub lTemp = pNubA;
                pNubA = pNubB;
                pNubB = lTemp;
            }

            if (pNubA.Type == Panel.Nub.NubType.Emitter)
            {
                string lEmitDereference = pNubA.Parent.Title + ":" + pNubA.Name;
                string lReceiveDereference = pNubB.Parent.Title + ":" + pNubB.Name;
                mEmits.RemoveAll(delegate(EmitConnection pSearchEmit)
                                 {
                                     return pSearchEmit.EmitDereference == lEmitDereference
                                            && pSearchEmit.ReceiveDereference == lReceiveDereference;
                                 });

                debug("removing connectin between emitter " + lEmitDereference + " and receiver " + lReceiveDereference);
            }
            else
            {
                string lFireDereference = pNubA.Parent.Title + ":" + pNubA.Name;
                FireConnection lFire = null;
                foreach (FireConnection lSearchFire in mFires.Where(delegate(FireConnection pSearchFire) { return pSearchFire.FireDereference == lFireDereference; }))
                    lFire = lSearchFire;

                if (lFire == null)
                    debug("ERROR: no fire connection pool for " + lFireDereference);
                else
                {
                    string lTriggerDereference = pNubB.Parent.Title + ":" + pNubB.Name;
                    lFire.TriggerDereference.RemoveAll(delegate(string pSearchTrigger) { return pSearchTrigger == lTriggerDereference; });
                    if (lFire.TriggerDereference.Count() <= 0)
                    {
                        mFires.Remove(lFire);
                        debug("removing fire connection pool for " + lFire.FireDereference);
                    }
                }
            }
            
            int lTotalTriggers = 0;
            foreach (FireConnection lFireConnection in mFires)
                lTotalTriggers += lFireConnection.TriggerDereference.Count();

            debug("total emits: " + mEmits.Count());
            debug("total fire pools: " + mFires.Count());
            debug("total fires: " + lTotalTriggers);
            debug("---------------------------");
        }

        public void Add(Panel pPanel)
        {
            LuaClass lClass = null;

            foreach (LuaClass lSearchClass in mClasses.Keys.Where(delegate(LuaClass pSearch) { return pSearch.Constructor == pPanel.Constructor; }))
                lClass = lSearchClass;

            if (lClass == null)
            {
                lClass = new LuaClass();
                lClass.Constructor = pPanel.Constructor;
                lClass.Code = pPanel.Code;
                mClasses[lClass] = 0;

                debug("caching new class <" + lClass.Constructor + ">");
                debug("code:\n @ " + lClass.Code.Replace("\n", "\n @ "));
            }
            else
            {
                debug("found <" + lClass.Constructor + "> in cache.");
            }

            mClasses[lClass]++;

            debug("there are now " + mClasses[lClass] + " instances of <" + lClass.Constructor + ">");
            debug("total cache size: " + mClasses.Keys.Count());

            LuaInstance lInstance = new LuaInstance();
            lInstance.Class = lClass;
            lInstance.InstanceID = pPanel.Title;
            mInstances.Add(lInstance);

            debug("adding instance data for " + lInstance.InstanceID + " of class <" + lInstance.Class.Constructor + ">");
            debug("total instances: " + mInstances.Count);
            debug("---------------------------");
        }

        public void Update(Panel pPanel)
        {
            if (!(pPanel is Constant))
                throw new Exception("Only constants can update their class definition after they've been placed.");

            LuaClass lClass = null;
            foreach (LuaClass lSearchClass in mClasses.Keys.Where(delegate(LuaClass pSearch) { return pSearch.Constructor == pPanel.Constructor; }))
                lClass = lSearchClass;

            if (lClass == null)
                debug("trying to update a panel that doesnt exist...");
            else
            {
                lClass.Constructor = pPanel.Constructor;
                lClass.Code = pPanel.Code;
            }
        }

        public void Remove(Panel pPanel)
        {
            LuaInstance lInstance = null;
            foreach (LuaInstance lSearchInstance in mInstances.Where(delegate(LuaInstance pSearchInstance) { return pSearchInstance.InstanceID == pPanel.Title; }))
                lInstance = lSearchInstance;

            if (lInstance == null)
                debug("ERROR: Tried to remove null instance " + pPanel.Title);
            else
            {
                mInstances.Remove(lInstance);
                debug("removing instance " + lInstance.InstanceID + " of class <" + lInstance.Class.Constructor + ">");
                mClasses[lInstance.Class]--;
                debug("there are now " + mClasses[lInstance.Class] + " instances of <" + lInstance.Class.Constructor + ">");
                debug("total instances: " + mInstances.Count);

                if (mClasses[lInstance.Class] <= 0)
                {
                    mClasses.Remove(lInstance.Class);
                    debug("decaching class <" + lInstance.Class.Constructor + ">");
                    debug("total cache size: " + mClasses.Keys.Count());
                }
            }

            debug("---------------------------");
        }
        #endregion

        public string Compile()
        {
            StringBuilder lCode = new StringBuilder();

            lCode.AppendLine("function unconnected() return \"unconnected\" end");

            lCode.AppendLine("\n-- =========== CLASSES =========== --\n");
            foreach (LuaClass lClass in mClasses.Keys)
                lCode.AppendLine(lClass.ToString());

            lCode.AppendLine("\n-- =========== INSTANCES =========== --\n");
            foreach (LuaInstance lInstance in mInstances)
                lCode.AppendLine(lInstance.ToString());

            lCode.AppendLine("\n-- =========== CONNECTIONS =========== --\n");
            foreach (EmitConnection lEmit in mEmits)
                lCode.AppendLine(lEmit.ToString());

            foreach (FireConnection lFire in mFires)
                lCode.AppendLine(lFire.ToString());

            // BUG: removing a start panel doesn't remove the corresponding jumpstart call

            lCode.AppendLine("\n-- =========== JUMPSTART =========== --\n");
            foreach (string lStart in mStart)
                lCode.AppendLine(lStart + "()");

            return lCode.ToString();
        }
    }
}